/*
 * Copyright 2010, Red Hat, Inc. and individual contributors as indicated by the
 * @author tags. See the copyright.txt file in the distribution for a full
 * listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This software is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
 * site: http://www.fsf.org.
 */
package org.zanata.service.impl;

import com.google.common.base.Optional;
import com.google.common.collect.MapMaker;
import lombok.extern.slf4j.Slf4j;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.xml.sax.InputSource;
import org.zanata.adapter.DTDAdapter;
import org.zanata.adapter.FileFormatAdapter;
import org.zanata.adapter.HTMLAdapter;
import org.zanata.adapter.IDMLAdapter;
import org.zanata.adapter.OpenOfficeAdapter;
import org.zanata.adapter.PlainTextAdapter;
import org.zanata.adapter.po.PoReader2;
import org.zanata.adapter.SubtitleAdapter;
import org.zanata.common.DocumentType;
import org.zanata.common.LocaleId;
import org.zanata.common.ProjectType;
import org.zanata.dao.DocumentDAO;
import org.zanata.dao.ProjectIterationDAO;
import org.zanata.exception.FileFormatAdapterException;
import org.zanata.exception.ZanataServiceException;
import org.zanata.model.HDocument;
import org.zanata.model.HProjectIteration;
import org.zanata.rest.dto.resource.Resource;
import org.zanata.rest.dto.resource.TranslationsResource;
import org.zanata.service.TranslationFileService;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static org.jboss.seam.ScopeType.STATELESS;
import static org.zanata.common.DocumentType.GETTEXT_PORTABLE_OBJECT;
import static org.zanata.common.DocumentType.GETTEXT_PORTABLE_OBJECT_TEMPLATE;
import static org.zanata.common.DocumentType.HTML;
import static org.zanata.common.DocumentType.IDML;
import static org.zanata.common.DocumentType.OPEN_DOCUMENT_GRAPHICS;
import static org.zanata.common.DocumentType.OPEN_DOCUMENT_PRESENTATION;
import static org.zanata.common.DocumentType.OPEN_DOCUMENT_SPREADSHEET;
import static org.zanata.common.DocumentType.OPEN_DOCUMENT_TEXT;
import static org.zanata.common.DocumentType.PLAIN_TEXT;
import static org.zanata.common.DocumentType.SUBTITLE;
import static org.zanata.common.DocumentType.XML_DOCUMENT_TYPE_DEFINITION;

/**
 * Default implementation of the TranslationFileService interface.
 *
 * @author Carlos Munoz <a
 *         href="mailto:camunoz@redhat.com">camunoz@redhat.com</a>
 */
@Name("translationFileServiceImpl")
@Scope(STATELESS)
@Slf4j
public class TranslationFileServiceImpl implements TranslationFileService {
    private static Map<DocumentType, Class<? extends FileFormatAdapter>> DOCTYPEMAP =
            new MapMaker().makeMap();
    private static DocumentType[] ODF_TYPES = { OPEN_DOCUMENT_TEXT,
            OPEN_DOCUMENT_PRESENTATION, OPEN_DOCUMENT_SPREADSHEET,
            OPEN_DOCUMENT_GRAPHICS };

    static {
        for (DocumentType type : ODF_TYPES) {
            DOCTYPEMAP.put(type, OpenOfficeAdapter.class);
        }
        DOCTYPEMAP.put(PLAIN_TEXT, PlainTextAdapter.class);
        DOCTYPEMAP.put(XML_DOCUMENT_TYPE_DEFINITION, DTDAdapter.class);
        DOCTYPEMAP.put(IDML, IDMLAdapter.class);
        DOCTYPEMAP.put(HTML, HTMLAdapter.class);
        DOCTYPEMAP.put(SUBTITLE, SubtitleAdapter.class);
    }

    private static Set<String> SUPPORTED_EXTENSIONS =
            buildSupportedExtensionSet();

    private static Set<String> buildSupportedExtensionSet() {
        Set<String> supported = new HashSet<String>();
        for (DocumentType type : DOCTYPEMAP.keySet()) {
            supported.addAll(type.getExtensions());
        }
        return supported;
    }

    @In
    private DocumentDAO documentDAO;

    @In
    private ProjectIterationDAO projectIterationDAO;

    @Override
    public TranslationsResource parseTranslationFile(InputStream fileContents,
            String fileName, String localeId, String projectSlug,
            String iterationSlug, String docId) throws ZanataServiceException {
        if (fileName.endsWith(".po")) {
            return parsePoFile(fileContents, projectSlug, iterationSlug, docId);
        } else if (hasAdapterFor(fileName)) {
            File tempFile = persistToTempFile(fileContents);
            TranslationsResource transRes =
                    parseAdapterTranslationFile(tempFile, projectSlug,
                            iterationSlug, docId, localeId, fileName);
            removeTempFile(tempFile);
            return transRes;
        } else {
            throw new ZanataServiceException("Unsupported Translation file: "
                    + fileName);
        }
    }

    @Override
    public TranslationsResource parsePoFile(InputStream fileContents,
            String projectSlug, String iterationSlug, String docId) {
        boolean originalIsPo = isPoDocument(projectSlug, iterationSlug, docId);
        try {
            return parsePoFile(fileContents, !originalIsPo);
        } catch (Exception e) {
            throw new ZanataServiceException(
                    "Invalid PO file contents on file: " + docId, e);
        }
    }

    @Override
    public TranslationsResource parseAdapterTranslationFile(File tempFile,
            String projectSlug, String iterationSlug, String docId,
            String localeId, String fileName) {
        Optional<String> params =
                documentDAO.getAdapterParams(projectSlug, iterationSlug, docId);
        TranslationsResource transRes;
        try {
            transRes =
                    getAdapterFor(fileName).parseTranslationFile(
                            tempFile.toURI(), localeId, params);
        } catch (FileFormatAdapterException e) {
            throw new ZanataServiceException("Error parsing translation file: "
                    + fileName, e);
        } catch (RuntimeException e) {
            throw new ZanataServiceException(e);
        }
        return transRes;
    }

    @Override
    public String generateDocId(String path, String fileName) {
        String docName = fileName;
        if (docName.endsWith(".pot")) {
            docName = docName.substring(0, docName.lastIndexOf('.'));
        }
        return convertToValidPath(path) + docName;
    }

    @Override
    public Resource parseUpdatedPotFile(InputStream fileContents, String docId,
            String fileName, boolean offlinePo) {
        if (fileName.endsWith(".pot")) {
            try {
                return parsePotFile(fileContents, docId, offlinePo);
            } catch (Exception e) {
                throw new ZanataServiceException(
                        "Invalid POT file contents on file: " + fileName, e);
            }
        } else {
            throw new ZanataServiceException("Unsupported Document file: "
                    + fileName);
        }
    }

    @Override
    public Resource parseAdapterDocumentFile(URI documentFile,
            String documentPath, String fileName, Optional<String> params)
            throws ZanataServiceException {
        return parseUpdatedAdapterDocumentFile(documentFile,
                convertToValidPath(documentPath) + fileName, fileName, params);
    }

    @Override
    public Resource parseUpdatedAdapterDocumentFile(URI documentFile,
            String docId, String fileName, Optional<String> params)
            throws ZanataServiceException {
        if (hasAdapterFor(fileName)) {
            FileFormatAdapter adapter = getAdapterFor(fileName);
            Resource doc;
            try {
                doc =
                        adapter.parseDocumentFile(documentFile, new LocaleId(
                                "en"), params);
            } catch (FileFormatAdapterException e) {
                throw new ZanataServiceException(
                        "Error parsing document file: " + fileName, e);
            }
            doc.setName(docId);
            return doc;
        } else {
            throw new ZanataServiceException("Unsupported Document file: "
                    + fileName);
        }
    }

    /**
     * A valid path is either empty, or has a trailing slash and no leading
     * slash.
     *
     * @param path
     * @return valid path
     */
    private String convertToValidPath(String path) {
        path = path.trim();
        while (path.startsWith("/")) {
            path = path.substring(1);
        }
        if (path.length() > 0 && !path.endsWith("/")) {
            path = path.concat("/");
        }
        return path;
    }

    private TranslationsResource parsePoFile(InputStream fileContents,
            boolean offlinePo) {
        PoReader2 poReader = new PoReader2(offlinePo);
        return poReader.extractTarget(new InputSource(fileContents));
    }

    private Resource parsePotFile(InputStream fileContents, String docId,
            boolean offlinePo) {
        PoReader2 poReader = new PoReader2(offlinePo);
        // assume english as source locale
        Resource res =
                poReader.extractTemplate(new InputSource(fileContents),
                        new LocaleId("en"), docId);
        return res;
    }

    // TODO replace these with values from DocumentType
    @Override
    public Set<String> getSupportedExtensions() {
        return SUPPORTED_EXTENSIONS;
    }

    @Override
    public boolean hasAdapterFor(DocumentType type) {
        if (type == null) {
            return false;
        }
        return DOCTYPEMAP.containsKey(type);
    }

    private boolean hasAdapterFor(String fileNameOrExtension) {
        String extension = extractExtension(fileNameOrExtension);
        if (extension == null) {
            return false;
        }
        DocumentType documentType = DocumentType.typeFor(extension);
        if (documentType == null) {
            return false;
        }
        return hasAdapterFor(documentType);
    }

    private FileFormatAdapter getAdapterFor(String fileNameOrExtension) {
        String extension = extractExtension(fileNameOrExtension);
        if (extension == null) {
            throw new RuntimeException(
                    "Cannot find adapter for null filename or extension.");
        }
        DocumentType documentType = DocumentType.typeFor(extension);
        if (documentType == null) {
            throw new RuntimeException(
                    "Cannot choose an adapter because the provided string '"
                            + fileNameOrExtension
                            + "' does not match any known document type.");
        }
        return getAdapterFor(documentType);
    }

    @Override
    public DocumentType getDocumentType(String fileNameOrExtension) {
        return DocumentType.typeFor(extractExtension(fileNameOrExtension));
    }

    @Override
    public FileFormatAdapter getAdapterFor(DocumentType type) {
        Class<? extends FileFormatAdapter> clazz = DOCTYPEMAP.get(type);
        if (clazz == null) {
            throw new RuntimeException("No adapter for document type: " + type);
        }
        try {
            return clazz.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(
                    "Unable to construct adapter for document type: " + type, e);
        }
    }

    @Override
    public String extractExtension(String fileNameOrExtension) {
        if (fileNameOrExtension == null || fileNameOrExtension.length() == 0
                || fileNameOrExtension.endsWith(".")) {
            // could throw exception here
            return null;
        }

        String extension;
        if (fileNameOrExtension.contains(".")) {
            extension =
                    fileNameOrExtension.substring(fileNameOrExtension
                            .lastIndexOf('.') + 1);
        } else {
            extension = fileNameOrExtension;
        }
        return extension;
    }

    @Override
    public File persistToTempFile(InputStream fileContents) {
        File tempFile = null;
        try {
            tempFile = File.createTempFile("zupload", ".tmp");
            byte[] buffer = new byte[4096]; // To hold file contents
            int bytesRead;
            FileOutputStream output = new FileOutputStream(tempFile);
            while ((bytesRead = fileContents.read(buffer)) != -1) {
                output.write(buffer, 0, bytesRead);
            }
            output.close();
        } catch (IOException e) {
            throw new ZanataServiceException(
                    "Error while writing uploaded file to temporary location",
                    e);
        }
        return tempFile;
    }

    @Override
    public void removeTempFile(File tempFile) {
        if (tempFile != null) {
            if (!tempFile.delete()) {
                log.warn(
                        "unable to remove temporary file {}, marking for delete on exit",
                        tempFile.getAbsolutePath());
                tempFile.deleteOnExit();
            }
        }
    }

    @Override
    public String getFileExtension(String projectSlug, String iterationSlug,
            String docPath, String docName) {
        HDocument doc =
                documentDAO.getByProjectIterationAndDocId(projectSlug,
                        iterationSlug, docPath + docName);
        return doc.getRawDocument().getType().getExtension();
    }

    @Override
    public boolean isPoDocument(String projectSlug, String iterationSlug,
            String docId) {
        HProjectIteration projectIteration =
                projectIterationDAO.getBySlug(projectSlug, iterationSlug);
        ProjectType projectType = projectIteration.getProjectType();
        if (projectType == null) {
            projectType = projectIteration.getProject().getDefaultProjectType();
        }
        if (projectType == ProjectType.Gettext
                || projectType == ProjectType.Podir) {
            return true;
        }

        if (projectType == ProjectType.File) {
            HDocument doc =
                    documentDAO.getByDocIdAndIteration(projectIteration, docId);
            if (doc.getRawDocument() == null) {
                // po is the only format in File projects for which no raw
                // document is stored
                return true;
            }

            // additional check in case we do start storing raw documents for po
            DocumentType docType = doc.getRawDocument().getType();
            return docType == GETTEXT_PORTABLE_OBJECT
                    || docType == GETTEXT_PORTABLE_OBJECT_TEMPLATE;
        }
        return false;
    }

}
